package bancosys.tec.services.input.driver.xls;

import static jmine.tec.services.impl.ServicesImplMessages.INPUT_CELL_FORMULA_CONTAINS_ERROR;
import static jmine.tec.services.impl.ServicesImplMessages.INPUT_ERROR_EXECUTING_FORMULA;
import static jmine.tec.services.impl.ServicesImplMessages.INPUT_UNKNOWN_CELL_TYPE;

import java.util.HashMap;
import java.util.Map;

import jmine.tec.utils.date.DateUtil;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
import org.apache.poi.hssf.usermodel.HSSFRichTextString;
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.CellReference;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellValue;

import bancosys.tec.services.input.exceptions.InputException;
import bancosys.tec.services.input.exceptions.InputFormulaException;

/**
 * @author Rafael Volpato (Jun 8, 2007)
 */
public final class XLSUtils {
    private final HSSFFormulaEvaluator formulaEvaluator;

    private final HSSFWorkbook workbook;

    private final Map<Short, String> fileFormatsCache = new HashMap<Short, String>();

    private static final Logger LOGGER = LogManager.getLogger(XLSUtils.class);

    /**
     * Construtor padrao.
     * 
     * @param workbook workbook
     */
    public XLSUtils(HSSFWorkbook workbook) {
        this.workbook = workbook;
        this.formulaEvaluator = new HSSFFormulaEvaluator(workbook);
    }

    /**
     * Retorna o conteudo da celula especificada na forma de String.
     * 
     * @param sheetName nome da aba aonde esta localizada a celula
     * @param row linha da planilha
     * @param pos posicao da coluna da celula
     * @return Conteudo da celula na forma de String
     * @throws InputException Se ocorrer algum erro durante a leitura do valor da celula
     */
    public String getCellContentAsString(String sheetName, HSSFRow row, int pos) throws InputException {
        String serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
        String cellContents = null;
        HSSFCell cell = row.getCell(pos);
        if (cell != null) {
            switch (cell.getCellType()) {
            case Cell.CELL_TYPE_NUMERIC:
                if (this.isCellDateFormatted(cell.getCellStyle().getDataFormat(), cell.getNumericCellValue())) {
                    cellContents = this.getFormattedDateCellValue(cell.getNumericCellValue());
                } else {
                    cellContents = this.getFormattedNumericCellValue(cell.getNumericCellValue(), cell.getCellStyle().getDataFormat());
                }
                break;

            case Cell.CELL_TYPE_BLANK:
            case Cell.CELL_TYPE_STRING:
                cellContents = cell.getRichStringCellValue().getString().trim();
                break;

            case Cell.CELL_TYPE_FORMULA:
                cellContents = this.getCellFormulaValue(sheetName, row, cell);
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                cellContents = Boolean.toString(cell.getBooleanCellValue());
                break;

            case Cell.CELL_TYPE_ERROR:
                throw new InputException(serviceLocation, INPUT_CELL_FORMULA_CONTAINS_ERROR.create(cell.getCellFormula()));

            default:
                serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
                throw new InputException(serviceLocation, INPUT_UNKNOWN_CELL_TYPE.create(cell.getCellType()));
            }
        }

        if (cellContents == null) {
            cellContents = "";
        }

        return cellContents;
    }

    /**
     * Converte a formula de uma celula em uma String.
     * 
     * @param sheetName sheetName
     * @param cell celula que possui a formula
     * @param row linha da celula
     * @return resultado da execução da formula
     * @throws InputException Se ocorrer algum erro durante a execução da formula
     */
    private String getCellFormulaValue(String sheetName, HSSFRow row, HSSFCell cell) throws InputException {
        String cellContents = null;
        CellValue cellValue = null;

        String serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), cell.getColumnIndex());
        try {
            cellValue = this.formulaEvaluator.evaluate(cell);
        } catch (Throwable t) {
            String formula = "Formula Invalida";
            try {
                formula = cell.getCellFormula() + "";
            } catch (Throwable t1) {
                t1.printStackTrace();
            }
            throw new InputFormulaException(serviceLocation, INPUT_ERROR_EXECUTING_FORMULA.create(formula), t);
        }

        if (cellValue == null) {
            throw new InputFormulaException(serviceLocation, INPUT_ERROR_EXECUTING_FORMULA.create(cell.getCellFormula()));
        }

        try {
            switch (cellValue.getCellType()) {
            case Cell.CELL_TYPE_BLANK:
            case Cell.CELL_TYPE_STRING:
                if (cellValue.getStringValue() != null) {
                    cellContents = cellValue.getStringValue().trim();
                }
                break;

            case Cell.CELL_TYPE_NUMERIC:
                if (this.isCellDateFormatted(cell.getCellStyle().getDataFormat(), cellValue.getNumberValue())) {
                    cellContents = this.getFormattedDateCellValue(cell.getNumericCellValue());
                } else {
                    // Buscar o valor pelo getNumericCellValue (que busca em um cache de fórmula) é mais preciso (bate com o valor que é
                    // exibido na planilha) mas existem casos esdruxulos.
                    try {
                        cellContents = this.getFormattedNumericCellValue(cell.getNumericCellValue(), cell.getCellStyle().getDataFormat());
                    } catch (IllegalStateException e) {
                        LOGGER.error(
                                "O tipo da célula com a fórmula não bate com o tipo de célula destino da fórmula. Isto gerou exceção. ["
                                        + serviceLocation + "]", e);
                        cellContents = this.getFormattedNumericCellValue(cellValue.getNumberValue(), cell.getCellStyle().getDataFormat());
                    }
                }
                break;

            case Cell.CELL_TYPE_BOOLEAN:
                cellContents = Boolean.toString(cellValue.getBooleanValue());
                break;

            case Cell.CELL_TYPE_ERROR:
                throw new InputException(serviceLocation, INPUT_CELL_FORMULA_CONTAINS_ERROR.create(cell.getCellFormula()));

            default:
                throw new InputException(serviceLocation, INPUT_UNKNOWN_CELL_TYPE.create(cellValue.getCellType()));
            }
        } catch (IllegalStateException e) {
            throw new InputException(serviceLocation, INPUT_ERROR_EXECUTING_FORMULA.create(), e);
        }

        return cellContents;
    }

    /**
     * Retorna o valor da célula no formato DD/MM/AAAA.
     * 
     * @param numericCellValue valor numérico da célula
     * @return o valor da célula no formato DD/MM/AAAA
     */
    private String getFormattedDateCellValue(Double numericCellValue) {
        return XLSFormatUtils.formatDateValue(numericCellValue);
    }

    /**
     * Retorna o valor numérico da célula respeitando a sua formatacao.
     * 
     * @param numericCellValue valor numero da celula
     * @param cellDataFormat formato do dado
     * @return valor da celula na forma de string.
     */
    private String getFormattedNumericCellValue(Double numericCellValue, short cellDataFormat) {
        return XLSFormatUtils.formatNumberValue(numericCellValue, this.getFormatString(cellDataFormat));
    }

    /**
     * Check if a cell contains a date Since dates are stored internally in Excel as double values we infer it is a date if it is formatted
     * as such.
     * 
     * @see org.apache.poi.hssf.usermodel.HSSFDateUtil.isCellDateFormatted(HSSFCell)
     * @param formatIndex the format index for the desired date format.
     * @param numericCellValue the data -> double value
     * @return true if the cell format is one of the valid date formats
     */
    public boolean isCellDateFormatted(short formatIndex, Double numericCellValue) {
        return XLSFormatUtils.isADateFormat(formatIndex, this.getFormatString(formatIndex), numericCellValue);
    }

    /**
     * Busca no cache e depois no workbook a mascara de formatacao.
     * 
     * @param idx indice da mascara
     * @return mascara de formatacao
     */
    private String getFormatString(short idx) {
        String fmtStr = this.fileFormatsCache.get(idx);
        if (fmtStr == null) {
            fmtStr = this.workbook.createDataFormat().getFormat(idx);

            // Não permitir formatação dupla, utilizar apenas a de valores positivos
            int pos = fmtStr.indexOf(';');
            if (pos != -1) {
                fmtStr = fmtStr.substring(0, pos);
            }

            this.fileFormatsCache.put(idx, fmtStr);
        }
        return fmtStr;
    }

    /**
     * Monta uma String padrão que define a localizacao de um servico na planilha.
     * 
     * @param sheetName sheetName
     * @param row linha da planilha
     * @param col coluna da planilha
     * @return localizacao do servico na planilha em um formato "human readable".
     */
    public String getServiceLocation(String sheetName, int row, int col) {
        StringBuilder sb = new StringBuilder();
        sb.append("(");
        sb.append(sheetName);
        sb.append(":");
        sb.append(new CellReference(row, col, false, false).formatAsString());
        sb.append(")");
        return sb.toString();
    }

    /**
     * Coloca o valor em um celula especificada na forma.
     * 
     * @param sheetName nome da aba aonde esta localizada a celula
     * @param row linha da planilha
     * @param pos posicao da coluna da celula
     * @param value Valor a ser colocado na celula
     * @throws InputException Se ocorrer algum erro durante a leitura do valor da celula
     */
    public void setCellContentFromString(String sheetName, HSSFRow row, int pos, String value) throws InputException {
        String serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
        HSSFCell cell = row.getCell(pos);

        if (cell != null) {
            switch (cell.getCellType()) {
            case Cell.CELL_TYPE_NUMERIC:
                if (this.isCellDateFormatted(cell.getCellStyle().getDataFormat(), new Double(value))) {
                    cell.setCellValue(DateUtil.safeParse(value, DateUtil.DATE_PATTERN_DDMMYYYY));
                } else {
                    cell.setCellValue(new Double(value));
                }
                break;

            case Cell.CELL_TYPE_BLANK:
            case Cell.CELL_TYPE_STRING:
                cell.setCellValue(new HSSFRichTextString(value));
                break;

            case Cell.CELL_TYPE_FORMULA:
                cell.setCellValue(new HSSFRichTextString(value));
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                if (value.equals("0") || value.equals("1")) {
                    throw new InputException(serviceLocation, INPUT_UNKNOWN_CELL_TYPE.create(cell.getCellType()));
                }
                cell.setCellValue(value.equals("1") ? true : false);
                break;

            case Cell.CELL_TYPE_ERROR:
                throw new InputException(serviceLocation, INPUT_CELL_FORMULA_CONTAINS_ERROR.create(cell.getCellFormula()));

            default:
                serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
                throw new InputException(serviceLocation, INPUT_UNKNOWN_CELL_TYPE.create());
            }
        }
    }

    /**
     * Retorna o conteudo da celula especificada na forma de String, com seu conteudo real, ex: formula e nao seu valor
     * 
     * @param sheetName nome da aba aonde esta localizada a celula
     * @param row linha da planilha
     * @param pos posicao da coluna da celula
     * @return Conteudo da celula na forma de String
     * @throws InputException Se ocorrer algum erro durante a leitura do valor da celula
     */
    public String getRealCellContentAsString(String sheetName, HSSFRow row, int pos) throws InputException {
        String serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
        String cellContents = null;
        HSSFCell cell = row.getCell(pos);
        if (cell != null) {
            switch (cell.getCellType()) {
            case Cell.CELL_TYPE_NUMERIC:
                if (this.isCellDateFormatted(cell.getCellStyle().getDataFormat(), cell.getNumericCellValue())) {
                    cellContents = this.getFormattedDateCellValue(cell.getNumericCellValue());
                } else {
                    cellContents = this.getFormattedNumericCellValue(cell.getNumericCellValue(), cell.getCellStyle().getDataFormat());
                }
                break;

            case Cell.CELL_TYPE_BLANK:
            case Cell.CELL_TYPE_STRING:
                cellContents = cell.getRichStringCellValue().getString().trim();
                break;

            case Cell.CELL_TYPE_FORMULA:
                cellContents = cell.getRichStringCellValue().getString().trim();
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                cellContents = Boolean.toString(cell.getBooleanCellValue());
                break;

            case Cell.CELL_TYPE_ERROR:
                throw new InputException(serviceLocation, INPUT_CELL_FORMULA_CONTAINS_ERROR.create(cell.getCellFormula()));

            default:
                serviceLocation = this.getServiceLocation(sheetName, row.getRowNum(), pos);
                throw new InputException(serviceLocation, INPUT_UNKNOWN_CELL_TYPE.create(cell.getCellType()));
            }
        }

        if (cellContents == null) {
            cellContents = "";
        }

        return cellContents;
    }
}
